Authentication bypass via OAuth implicit flow
Bypass de autenticación abusando del flujo OAuth implícito: la app cliente confía en el email enviado a /authenticate sin vincularlo al access token, permitiendo suplantar la identidad de carlos.
Target Overview & Vulnerability Summary
El laboratorio permite el login con una cuenta de red social vía un servicio OAuth. Una validación defectuosa por parte de la aplicación cliente permite a un atacante iniciar sesión en la cuenta de otro usuario sin conocer su password: el endpoint /authenticate confía en el email que el cliente envía y lo usa para crear la sesión, sin verificar que el access token corresponda realmente a ese email.
| Campo | Valor |
|---|---|
| Severidad | Alta (account takeover: login en la cuenta de carlos sin credenciales) |
| Endpoint vulnerable | POST /authenticate (creación de sesión), GET /auth (autorización OAuth) |
| Root cause | El cliente crea la sesión con un email declarado por el atacante, sin validar que el access token pertenezca a esa identidad |
Technical Breakdown & Exploitation Flow
Credenciales de laboratorio: wiener:peter. Email objetivo: carlos@carlos-montoya.net.
Flujo OAuth legítimo
Al iniciar sesión con las credenciales propias se sigue la secuencia:
GET /my-account- Redirección a
GET /social-login GET /authhacia el servidor OAuth con el grant implicit:
GET /auth?client_id=kf8dlbsnofv01tyhfe43n&redirect_uri=https://0a7400930475bcb8805d1705004a0003.web-security-academy.net/oauth-callback&response_type=token&nonce=-6836096&scope=openid%20profile%20email HTTP/2
Respuesta con la cookie de interacción del servidor OAuth:
Set-Cookie: _interaction=UYxHDxXsg5L3b1NMnMtEr;
GET /interaction/UYxHDxXsg5L3b1NMnMtEr(con la cookie_interaction) presenta el formulario de login del proveedor OAuth.POST /interaction/UYxHDxXsg5L3b1NMnMtEr/login:
username=wiener&password=peter
- Consentimiento en
GET /auth/UYxHDxXsg5L3b1NMnMtEr.
Semántica de los parámetros del flujo:
client_id: identifica la app cliente; se genera cuando la app se registra con el servicio OAuth.redirect_uri: URI a la que el navegador es redirigido al finalizar, donde se entrega la respuesta a la app cliente (CALLBACK URI / CALLBACK ENDPOINT).response_type: determina el tipo de respuesta esperada. Con valortoken, se usa el grant type implícito (el token se entrega directamente en el fragment de la URL, sin code exchange).state(OAuth 2.0): valor único e inadivinable enlazado a la sesión de la app cliente; el proveedor debe devolverlo junto al code, funcionando como token CSRF para proteger el callback.nonce(OpenID Connect): enlaza el token con el cliente y sirve como parámetro de validación del token (anti-replay delid_token).scope: especifica qué subconjunto de datos del usuario solicita la app cliente.
Al conceder el acceso, el navegador es redirigido a una URL cuyo fragment contiene el access_token, extraído por JavaScript del lado cliente:
Redirecting to https://0ae00012030597c3814e2f9700440021.web-security-academy.net/oauth-callback#access_token=bY66yHIhrP95bFDWfJZsFVJSzHzPyYwU3pgCMKCe53X&expires_in=3600&token_type=Bearer&scope=openid%20profile%20email.
El endpoint vulnerable /authenticate
El navegador envía el token extraído del fragment al endpoint de la app cliente:
POST /authenticate HTTP/2
{"email":"wiener@hotdog.com","username":"wiener","token":"bY66yHIhrP95bFDWfJZsFVJSzHzPyYwU3pgCMKCe53X"}
RESPONSE:
Set-Cookie: session=03iJIj8DY2kTWZg0m2oJ4sOjA9qtd5Gz;
La aplicación necesita un mecanismo para almacenar las sesiones; en este caso no usa passwords. Al cerrar sesión y volver a autenticarse, el cliente vuelve a emitir un POST /authenticate. Este request es manipulable: el campo email se puede cambiar a carlos@carlos-montoya.net y, como la app confía en ese valor para crear la sesión, se obtiene la sesión implícitamente confiada de carlos.
- Completar el flujo OAuth legítimo con
wiener:peterpara obtener unaccess_tokenválido. - Interceptar el
POST /authenticatey reemplazaremailporcarlos@carlos-montoya.net. - La respuesta entrega
Set-Cookie: session=...con la sesión decarlos. El laboratorio se resuelve con el login en la cuenta decarlos(su email escarlos@carlos-montoya.net).
Root Cause Analysis
En este escenario, POST /authenticate equivale a completar un formulario usuario/password, pero no existe ningún secreto que comparar: la identidad se confía de forma implícita porque la app cliente no valida la relación entre el access token y el email declarado. El patrón defectuoso es usar el email del body como fuente de identidad sin verificar la identidad real del titular del token (p. ej. consultando el endpoint userinfo del proveedor con el token):
// defectuoso: la identidad la define el campo email del cliente
var req struct{ Email, Username, Token string }
json.NewDecoder(r.Body).Decode(&req)
if tokenStore.Valid(req.Token) { // solo valida que el token exista
session.User = lookupByEmail(req.Email) // pero no que el token pertenezca a ese email
}
Además, el grant type implicit entrega el access token en el fragment de la URL (response_type=token), quedando expuesto a historial, logs de referer y extensión, y sin el code intercambiable que sí permitiría validar la identidad server-side.
Secure Remediation
- Vincular el token a la identidad: usar el access token para consultar
userinfodel proveedor (GET /userinfoconAuthorization: Bearer <token>) y derivar el email/subdesde esa respuesta, nunca desde el body del cliente:
identity, err := oauth.UserInfo(req.Token) // sub/email resueltos server-side
if err != nil {
return http.StatusUnauthorized
}
session.User = identity.Email
- Migrar al authorization code flow (
response_type=code+ PKCE): el code se intercambia server-side y el token nunca transita por el navegador; el servidor puede validaraud,issynonce. - Validar
nonceystateen el callback, y confirmar que el token fue emitido para esteclient_id. - Almacenar el token enlazado a la sesión y a la identidad verificada, nunca confiar en campos de identidad del body.